feat(gh-copilot): add AI credit usage billing metrics and fix report-body double-read - #9019
Merged
klesh merged 4 commits intoJul 30, 2026
Conversation
Adds collection and extraction of GitHub Copilot AI credit usage billing metrics at org, user, and enterprise levels, including new models, tasks (ai_credit_collector/extractor), a migration for the new billing tables, and e2e snapshot fixtures. Ports the work from apache#8980. Co-authored-by: Andrei Savu <54935810+AndreiS-gh@users.noreply.github.com> Signed-off-by: Pankaj Chaudhari <pankaj.chaudhari@atos.net>
The user and enterprise Copilot metrics collectors read the report metadata HTTP response body with io.ReadAll, then called parseReportMetadataResponse(res, ...) which read res.Body a second time. Since the body was already consumed, the second read returned empty, parseReportMetadata logged "Report metadata response was empty, skipping" and returned nil, so the collectors produced zero records. Parse the metadata from the body already read instead. The organization collector was unaffected because it reads the body only once inline. Fixes empty _raw_copilot_user_metrics and _raw_copilot_enterprise_metrics (and downstream _tool_copilot_user_daily_metrics) while org metrics work. Signed-off-by: Pankaj Chaudhari <pankaj.chaudhari@atos.net>
- migrationscripts: inline snapshot structs instead of importing models pkg - user_metrics: fix gofmt struct-tag alignment - ai_credit_collector/extractor: fix ASF license header (capital The) - report_download_helper: remove unused readReportMetadataBody/parseReportMetadataResponse - metrics_collector_test: retarget tests to parseReportMetadata/ignoreNoContent
klesh
approved these changes
Jul 30, 2026
klesh
left a comment
Contributor
There was a problem hiding this comment.
LGTM.
Thanks for your contribution.
PankajChaudhari-Atos
deleted the
feat-gh-copilot-ai-credit-and-report-fix
branch
July 30, 2026 16:30
DoDiODev
added a commit
to DoDiODev/devlake
that referenced
this pull request
Aug 3, 2026
The cross-plugin schema-drift guard added by this PR caught a fourth occurrence of the same bug class, introduced by apache#9019: _tool_copilot_enterprise_ai_credit_usage _tool_copilot_org_ai_credit_usage _tool_copilot_user_ai_credit_usage all lack gross_quantity, discount_quantity, net_quantity, price_per_unit, gross_amount, discount_amount and net_amount, while the runtime models models.GhCopilot{Enterprise,Org,User}AiCreditUsage declare them. Writing a record therefore fails with "Unknown column 'gross_quantity' in 'field list'". Root cause: 20260708_add_ai_credit_usage_metrics.go declares those seven columns through an anonymous embedded struct whose TYPE NAME IS UNEXPORTED creditUsageBreakdown20260708 `gorm:"embedded"` and GORM's schema parser skips anonymous fields of unexported types, so AutoMigrate never created the columns. Add a new, additive migration that re-runs AutoMigrateTables on structs which embed an EXPORTED type, so the columns are actually picked up. AutoMigrate only adds absent columns, so this is safe on populated tables. The original script is left untouched: migration scripts are append-only and its version is already recorded in _devlake_migration_history. Verified with the cross-plugin guard on MySQL 8.4.10 and PostgreSQL 17.2: 44/44 plugins pass (was 43/44 with gh-copilot failing). Signed-off-by: DoDiODev <DoDiDev@proton.me>
DoDiODev
added a commit
to DoDiODev/devlake
that referenced
this pull request
Aug 3, 2026
The cross-plugin schema-drift guard added by this PR caught a fourth occurrence of the same bug class, introduced by apache#9019: _tool_copilot_enterprise_ai_credit_usage _tool_copilot_org_ai_credit_usage _tool_copilot_user_ai_credit_usage all lack gross_quantity, discount_quantity, net_quantity, price_per_unit, gross_amount, discount_amount and net_amount, while the runtime models models.GhCopilot{Enterprise,Org,User}AiCreditUsage declare them inline. Writing a record therefore fails with "Unknown column 'gross_quantity' in 'field list'". Root cause: 20260708_add_ai_credit_usage_metrics.go declares those seven columns through an anonymous embedded struct whose TYPE NAME IS UNEXPORTED creditUsageBreakdown20260708 `gorm:"embedded"` and GORM's schema parser skips anonymous fields of unexported types, so AutoMigrate never created the columns. Add a new, additive migration that AutoMigrates the missing columns. It only adds absent columns, so it is a no-op on databases that already have them and safe on populated tables. The original script is left untouched: migration scripts are append-only and its version is already recorded in _devlake_migration_history. Verified with the cross-plugin guard against a fresh database on MySQL 8.4.10 and PostgreSQL 17.2: 44/44 plugins pass (was 43/44 with gh-copilot failing on 21 missing columns). Signed-off-by: DoDiODev <DoDiDev@proton.me>
klesh
pushed a commit
that referenced
this pull request
Aug 3, 2026
…ts (#9015) * fix(jira): add missing _raw_data_* columns to _tool_jira_sprint_reports The Sprint Report migration 20260722_add_sprint_report_table.go creates _tool_jira_sprint_reports from a struct that does not embed common.NoPKModel, while the runtime model models.JiraSprintReport does. The columns _raw_data_params / _raw_data_table / _raw_data_id / _raw_data_remark (plus created_at, updated_at) were therefore never created, so the ApiExtractor cleanup query WHERE _raw_data_table = ? AND _raw_data_params = ? made the extractSprintReport subtask fail with "Error 1054 (42S22): Unknown column '_raw_data_table' in 'where clause'". Add a new, additive migration that re-runs AutoMigrateTables on a struct embedding archived.NoPKModel. The original migration is left untouched: migration scripts are append-only, and editing it would not repair databases that already recorded its version. Add two schema-drift regression guards that run the REAL migration scripts instead of AutoMigrate-ing the runtime model, which would hide this class of drift: * plugins/jira/e2e/migration_schema_test.go - Jira-specific guard. * plugins/schema_e2e/migration_schema_test.go - cross-plugin guard for every built-in Go plugin, including TestAllGoPluginsListed so the guard stays complete when a new plugin is added. Both guards run the migrations against a dedicated, empty database created by the new helper e2ehelper.NewIsolatedMigrationDb: the shared e2e database is polluted by the other e2e tests, which AutoMigrate tables without recording anything in _devlake_migration_history, so running the real scripts against it fails with errors such as "Table 'cicd_pipeline_commits' already exists". The cross-plugin guard immediately uncovered three pre-existing drifts of the same class, each fixed with its own additive migration: * _tool_taiga_scope_configs - missing type_mappings * _tool_teambition_scope_configs - missing id, created_at, updated_at * _tool_testmo_scope_configs - missing connection_id, name The teambition table has no primary key at all, and its missing `id` is an auto-increment primary key, which AutoMigrate cannot append to an existing table (MySQL: "Incorrect table definition; there can be only one auto column and it must be defined as a key"). That column is therefore added with explicit DDL, which also keeps the ids of existing rows and the sequence/counter in sync on both MySQL and PostgreSQL. Finally, exclude plugins/schema_e2e from scripts/build-plugins.sh: it is not a plugin and has no main package, which broke `make build-plugin` with "-buildmode=plugin requires exactly one main package". Signed-off-by: DoDiODev <DoDiDev@proton.me> * fix(gh-copilot): add missing AI credit usage breakdown columns The cross-plugin schema-drift guard added by this PR caught a fourth occurrence of the same bug class, introduced by #9019: _tool_copilot_enterprise_ai_credit_usage _tool_copilot_org_ai_credit_usage _tool_copilot_user_ai_credit_usage all lack gross_quantity, discount_quantity, net_quantity, price_per_unit, gross_amount, discount_amount and net_amount, while the runtime models models.GhCopilot{Enterprise,Org,User}AiCreditUsage declare them inline. Writing a record therefore fails with "Unknown column 'gross_quantity' in 'field list'". Root cause: 20260708_add_ai_credit_usage_metrics.go declares those seven columns through an anonymous embedded struct whose TYPE NAME IS UNEXPORTED creditUsageBreakdown20260708 `gorm:"embedded"` and GORM's schema parser skips anonymous fields of unexported types, so AutoMigrate never created the columns. Add a new, additive migration that AutoMigrates the missing columns. It only adds absent columns, so it is a no-op on databases that already have them and safe on populated tables. The original script is left untouched: migration scripts are append-only and its version is already recorded in _devlake_migration_history. Verified with the cross-plugin guard against a fresh database on MySQL 8.4.10 and PostgreSQL 17.2: 44/44 plugins pass (was 43/44 with gh-copilot failing on 21 missing columns). Signed-off-by: DoDiODev <DoDiDev@proton.me> * test(schema): guard the migration upgrade path on populated tables Review feedback on #9015: TestMigrationSchemaMatchesModels proves the END STATE of a fresh migration run matches the runtime models, but every table it inspects is empty, so it never exercises the upgrade path of a repair migration on a database that already holds rows -- which is the only situation those migrations exist for. Add TestMigrationUpgradePathOnPopulatedTables, which for every repair migration in this PR 1. recreates the table exactly as the buggy migration left it, 2. inserts rows, 3. runs ONLY that repair script, 4. asserts the columns were added, the rows survived, the table has a primary key and auto-increment ids were backfilled (plus that a subsequent INSERT still works, i.e. the sequence/counter is in sync). Step 4 covers what a column-presence check cannot see. Negative test, with the explicit AUTO_INCREMENT DDL in the teambition script replaced by a plain AutoMigrate: MySQL -> FAIL, migration errors out (Error 1075) PostgreSQL -> FAIL, "table has no primary key after ..." (AutoMigrate happily adds `bigserial` without a key, so this is invisible to the column-only guard) Covered: _tool_jira_sprint_reports, _tool_taiga_scope_configs, _tool_teambition_scope_configs, _tool_testmo_scope_configs and the three _tool_copilot_*_ai_credit_usage tables. The scripts are looked up through each plugin's own MigrationScripts() by version, so the test fails if one is removed or renumbered. Verified on MySQL 8.4.10 and PostgreSQL 17.2: 51/51 subtests pass (44 plugins + 7 upgrade-path cases). Signed-off-by: DoDiODev <DoDiDev@proton.me> --------- Signed-off-by: DoDiODev <DoDiDev@proton.me>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR bundles two related gh-copilot changes (18 files):
feat: AI credit usage billing metrics (ports feat(gh-copilot): add AI credit usage billing metrics collection #8980)
Adds collection/extraction of GitHub Copilot AI credit usage billing
metrics at org, user, and enterprise levels — new models, tasks
(
ai_credit_collector/ai_credit_extractor), a migration for the newbilling tables, and e2e snapshot fixtures.
fix: report-body double-read zeroing Copilot user/enterprise metrics
parseUserMetricsReportResponseandparseRawReportResponseread theHTTP response body once (via
io.ReadAll) and then calledparseReportMetadataResponse(res, ...), which re-read the already-consumedbody. The second read returned empty → "Report metadata response was empty,
skipping" → 0 records, silently zeroing user and enterprise Copilot metrics
(org metrics were unaffected because that path reads the body once inline).
Fix: parse the already-read
bodyviaparseReportMetadata(body, logger)and drop the redundant re-read.
Testing
go build ./plugins/gh-copilot/...,go vet, andgo test ./plugins/gh-copilot/models/...pass.collection logged "empty, skipping" and produced 0 rows; after the fix, it
downloads report files and populates
_raw_copilot_user_metricsand_tool_copilot_user_daily_metrics.Notes
Co-authored-by.